<!--http://konvajs-doc.bluehymn.com/docs/sandbox/Objects_Snapping.html-->

<!DOCTYPE html>
<html>
<head>
  <script src="https://unpkg.com/konva@4.0.18/konva.min.js"></script>
  <meta charset="utf-8" />
  <title>Konva Snapping of shapes Demo</title>
  <style>
    body {
      margin: 0;
      padding: 0;
      overflow: hidden;
      background-color: #f0f0f0;
    }
  </style>
</head>
<body>
<div id="container"></div>
<script>
  var width = window.innerWidth;
  var height = window.innerHeight;
  var GUIDELINE_OFFSET = 5;

  var stage = new Konva.Stage({
    container: 'container',
    width: width,
    height: height
  });

  var layer = new Konva.Layer();
  stage.add(layer);

  // first generate random rectangles
  for (var i = 0; i < 5; i++) {
    layer.add(
      new Konva.Rect({
        x: Math.random() * stage.width(),
        y: Math.random() * stage.height(),
        width: 50 + Math.random() * 50,
        height: 50 + Math.random() * 50,
        fill: Konva.Util.getRandomColor(),
        rotation: Math.random() * 360,
        draggable: true,
        name: 'object'
      })
    );
  }

  // were can we snap our objects?
  function getLineGuideStops(skipShape) {
    // we can snap to stage borders and the center of the stage
    var vertical = [0, stage.width() / 2, stage.width()];
    var horizontal = [0, stage.height() / 2, stage.height()];

    // and we snap over edges and center of each object on the canvas
    stage.find('.object').forEach(guideItem => {
      if (guideItem === skipShape) {
        return;
      }
      var box = guideItem.getClientRect();
      // and we can snap to all edges of shapes
      vertical.push([box.x, box.x + box.width, box.x + box.width / 2]);
      horizontal.push([box.y, box.y + box.height, box.y + box.height / 2]);
    });
    return {
      vertical: vertical.flat(),
      horizontal: horizontal.flat()
    };
  }

  // what points of the object will trigger to snapping?
  // it can be just center of the object
  // but we will enable all edges and center
  function getObjectSnappingEdges(node) {
    var box = node.getClientRect();
    return {
      vertical: [
        {
          guide: Math.round(box.x),
          offset: Math.round(node.x() - box.x),
          snap: 'start'
        },
        {
          guide: Math.round(box.x + box.width / 2),
          offset: Math.round(node.x() - box.x - box.width / 2),
          snap: 'center'
        },
        {
          guide: Math.round(box.x + box.width),
          offset: Math.round(node.x() - box.x - box.width),
          snap: 'end'
        }
      ],
      horizontal: [
        {
          guide: Math.round(box.y),
          offset: Math.round(node.y() - box.y),
          snap: 'start'
        },
        {
          guide: Math.round(box.y + box.height / 2),
          offset: Math.round(node.y() - box.y - box.height / 2),
          snap: 'center'
        },
        {
          guide: Math.round(box.y + box.height),
          offset: Math.round(node.y() - box.y - box.height),
          snap: 'end'
        }
      ]
    };
  }

  // find all snapping possibilities
  function getGuides(lineGuideStops, itemBounds) {
    var resultV = [];
    var resultH = [];

    lineGuideStops.vertical.forEach(lineGuide => {
      itemBounds.vertical.forEach(itemBound => {
        var diff = Math.abs(lineGuide - itemBound.guide);
        // if the distance between guild line and object snap point is close we can consider this for snapping
        if (diff < GUIDELINE_OFFSET) {
          resultV.push({
            lineGuide: lineGuide,
            diff: diff,
            snap: itemBound.snap,
            offset: itemBound.offset
          });
        }
      });
    });

    lineGuideStops.horizontal.forEach(lineGuide => {
      itemBounds.horizontal.forEach(itemBound => {
        var diff = Math.abs(lineGuide - itemBound.guide);
        if (diff < GUIDELINE_OFFSET) {
          resultH.push({
            lineGuide: lineGuide,
            diff: diff,
            snap: itemBound.snap,
            offset: itemBound.offset
          });
        }
      });
    });

    var guides = [];

    // find closest snap
    var minV = resultV.sort((a, b) => a.diff - b.diff)[0];
    var minH = resultH.sort((a, b) => a.diff - b.diff)[0];
    if (minV) {
      guides.push({
        lineGuide: minV.lineGuide,
        offset: minV.offset,
        orientation: 'V',
        snap: minV.snap
      });
    }
    if (minH) {
      guides.push({
        lineGuide: minH.lineGuide,
        offset: minH.offset,
        orientation: 'H',
        snap: minH.snap
      });
    }
    return guides;
  }

  function drawGuides(guides) {
    guides.forEach(lg => {
      if (lg.orientation === 'H') {
        var line = new Konva.Line({
          points: [-6000, lg.lineGuide, 6000, lg.lineGuide],
          stroke: 'rgb(0, 161, 255)',
          strokeWidth: 1,
          name: 'guid-line',
          dash: [4, 6]
        });
        layer.add(line);
        layer.batchDraw();
      } else if (lg.orientation === 'V') {
        var line = new Konva.Line({
          points: [lg.lineGuide, -6000, lg.lineGuide, 6000],
          stroke: 'rgb(0, 161, 255)',
          strokeWidth: 1,
          name: 'guid-line',
          dash: [4, 6]
        });
        layer.add(line);
        layer.batchDraw();
      }
    });
  }

  layer.on('dragmove', function(e) {
    // clear all previous lines on the screen
    layer.find('.guid-line').destroy();

    // find possible snapping lines
    var lineGuideStops = getLineGuideStops(e.target);
    // find snapping points of current object
    var itemBounds = getObjectSnappingEdges(e.target);

    // now find where can we snap current object
    var guides = getGuides(lineGuideStops, itemBounds);

    // do nothing of no snapping
    if (!guides.length) {
      return;
    }

    drawGuides(guides);

    // now force object position
    guides.forEach(lg => {
      switch (lg.snap) {
        case 'start': {
          switch (lg.orientation) {
            case 'V': {
              e.target.x(lg.lineGuide + lg.offset);
              break;
            }
            case 'H': {
              e.target.y(lg.lineGuide + lg.offset);
              break;
            }
          }
          break;
        }
        case 'center': {
          switch (lg.orientation) {
            case 'V': {
              e.target.x(lg.lineGuide + lg.offset);
              break;
            }
            case 'H': {
              e.target.y(lg.lineGuide + lg.offset);
              break;
            }
          }
          break;
        }
        case 'end': {
          switch (lg.orientation) {
            case 'V': {
              e.target.x(lg.lineGuide + lg.offset);
              break;
            }
            case 'H': {
              e.target.y(lg.lineGuide + lg.offset);
              break;
            }
          }
          break;
        }
      }
    });
  });

  layer.on('dragend', function(e) {
    // clear all previous lines on the screen
    layer.find('.guid-line').destroy();
    layer.batchDraw();
  });

  layer.draw();
  console.log(stage.toJSON())
</script>
</body>
</html>
